COMP3161/9164 Concepts of Programming Languages
Term 3, 2024

Code and Notes (Week 1 Tuesday)

Table of Contents

1. Big Step and Small Step in Haskell

I fixed my bug after the lecture. In the big-step semantics, I had substituted in e2 to create e2subst, but then called big-step with the original e2 value. Oops.

I also adapted the lexer/parser from the previous lecture so that we can use concrete syntax for let-expressions.

import Data.Char

data Arith = Num Int | Var String
  | Plus Arith Arith | Times Arith Arith
  | Let String Arith Arith
  deriving Show


big_step :: Arith -> Int
big_step (Num i) = i
big_step (Plus e1 e2) = big_step e1 + big_step e2
big_step (Times e1 e2) = big_step e1 * big_step e2
big_step (Let nm e1 e2) =
  let v1 = big_step e1 in
  let e2_subst = subst nm (Num v1) e2 in
  big_step e2_subst
big_step (Var x) = error ("big_step: encountered var " ++ x)

-- substitute x for variables named nm in y
subst :: String -> Arith -> Arith -> Arith
subst nm x y = case y of
  Num _ -> y
  Plus a b -> Plus (subst nm x a) (subst nm x b)
  Times a b -> Times (subst nm x a) (subst nm x b)
  Let nm2 a b -> Let nm2 (subst nm x a)
    (if nm == nm2 then b else subst nm x b)
  Var nm2 -> if nm == nm2 then x else y

example_expr1 = Let "x"
    (Let "y" (Num 4) (Plus (Var "y") (Num 3))) (Var "x")
example_string1 = "let x = (let y = 4 in y + 3) in x"
example_string2 = "let x = (let y = 5 + 6 in y + y + 3) in x + 5 + (let z = 3 in z + 2)"


small_step :: Arith -> Maybe Arith
small_step (Num i) = Nothing
small_step (Plus (Num i) (Num j)) = Just (Num (i + j))
small_step (Plus (Num i) e2) = case small_step e2 of
    Just e2' -> Just (Plus (Num i) e2')
    Nothing -> Nothing
small_step (Plus e1 e2) = case small_step e1 of
    Just e1' -> Just (Plus e1' e2)
    Nothing -> Nothing
small_step (Times (Num i) (Num j)) = Just (Num (i + j))
small_step (Times (Num i) e2) = case small_step e2 of
    Just e2' -> Just (Times (Num i) e2')
    Nothing -> Nothing
small_step (Times e1 e2) = case small_step e1 of
    Just e1' -> Just (Times e1' e2)
    Nothing -> Nothing
small_step (Let nm (Num i) e2) = Just (subst nm (Num i) e2)
small_step (Let nm e1 e2) = case small_step e1 of
    Just e1' -> Just (Let nm e1' e2)
    Nothing -> Nothing
small_step (Var x) = error ("small_step: encountered var " ++ x)


small_steps (Num i) = [Num i]
small_steps e = case small_step e of
    Nothing -> error ("couldn't step: " ++ show e)
    Just e2 -> e : small_steps e2


data Token = LitT Int
  | LParenT | RParenT
  | EqualsT | PlusT | TimesT
  | LetT | InT | VarT String
  deriving Show

lexer :: String -> [Token]
lexer ('(' : cs) = LParenT : lexer cs
lexer (')' : cs) = RParenT : lexer cs
lexer ('+' : cs) = PlusT : lexer cs
lexer ('*' : cs) = TimesT : lexer cs
lexer ('=' : cs) = EqualsT : lexer cs
lexer [] = []
lexer (c : cs) = if isSpace c then lexer cs
  else if isDigit c
  then let (int_string, rest) = break (not . isDigit) (c : cs)
       in (LitT (read int_string) : lexer rest)
  else if isAlpha c
  then let (ident_str, rest) = break isSpace (c : cs)
       in (token_of_ident ident_str : lexer rest)
  else error ("couldn't lex this: " ++ show (c : cs))

token_of_ident s = if s == "let" then LetT else if s == "in" then InT
    else VarT s

parser_atom :: [Token] -> (Arith, [Token])
parser_atom (LitT i : ts) = (Num i, ts)
parser_atom (VarT nm : ts) = (Var nm, ts)
parser_atom (LParenT : ts) =
    let (expr, ts2) = parser_lexpr ts
    in case ts2 of
        RParenT : ts3 -> (expr, ts3)
        ts3 -> error ("expecting right parenthesis at: " ++ show ts3)
parser_atom ts = error ("not an atomic expression: " ++ show ts)

parser_pexpr :: [Token] -> (Arith, [Token])
parser_pexpr ts =
    let (expr, ts2) = parser_atom ts
    in case ts2 of
        TimesT : ts3 ->
            let (expr2, ts4) = parser_pexpr ts3
            in (Times expr expr2, ts4)
        _ -> (expr, ts2)

parser_sexpr :: [Token] -> (Arith, [Token])
parser_sexpr ts =
    let (expr, ts2) = parser_pexpr ts
    in case ts2 of
        PlusT : ts3 ->
            let (expr2, ts4) = parser_sexpr ts3
            in (Plus expr expr2, ts4)
        _ -> (expr, ts2)

parser_lexpr :: [Token] -> (Arith, [Token])
parser_lexpr (LetT : VarT nm : EqualsT : ts) =
    let (a, b) = error "matched a let _ = beginning" in
    let (expr, ts2) = parser_sexpr ts
    in case ts2 of
        InT : ts3 ->
            let (expr2, ts4) = parser_sexpr ts3
            in (Let nm expr expr2, ts4)
        ts3 -> error ("expecting 'in' token at: " ++ show ts3)
parser_lexpr ts = parser_sexpr ts

parser :: String -> Arith
parser s = case parser_lexpr (lexer s) of
  (expr, []) -> expr
  (expr, ts) -> error ("left-over tokens after parsing: " ++ show ts)


2. Notes on a Proof of Big-Step/Small-Step equivalence


The goal is to prove the big-step and small-step
semantics are equivalent.


Let's start with the second case,

e →* Num n implies e ↓ n

The only sensible thing to do (I claim) is to induct on
the assumption e →* Num n, by rule induction using
the characterisation of reflexive transitive closure.

The induction ought (I claim) be used with a more general
assumption e →* e2, so that e and e2 can be instantiated
by the cases.

To do this, we prove the equivalent goal,
e →* e2 implies (!n. e2 = Num n implies e ↓ n),
using rule induction with P(e1, e2) ≡ (∀n. e2 = Num n implies e1 ↓ n).


The base case is the reflexive case, e →* e.

We must show P(e, e) ≡ (∀n. e = Num n implies e ↓ n),
which is exactly the Num case of the big-step rules.


The inductive case is associated with the step case
of the reflexive closure of the relation:

  e1 |-> e2   e2 |->* e3
  ----------------------
      e1 |->* e3

Our goal replaces the |->* judgements with P, that is,
we may assume e1 |-> e2 (the case assumption) and
P (e2, e3) (the inductive hypothesis).

We must show P(e1, e3) ≡ (!n. e3 = Num n implies e1 ↓ n).

So, we fix some n and assume e3 = Num n (call this the e3 premise).
We must show e1 ↓ n.

Our IH is P(e2, e2) ≡ (!n. e3 = Num n implies e2 ↓ n).
We pick the same n and combine this with the e3 premise to get e2 ↓ n.

Now we need to make use of our assumption e1 |-> e2. The small-step
relation is itself defined recursively for the various context cases
(which establish that we can small-step a compound expression by making
a small-step change at one of its inner expressions). The only sensible
thing to do (I claim) is to induct again.

Let us do this in the proof of a lemma that captures our current goal.
This lemma was shown on the slides:

   e |-> e'    e' ↓ n
   ------------------
         e ↓ n

This lemma makes intuitive sense. If we can adjust an expression via a
small-step, it should still evaluate to the same thing.

I claim that, for the induction to work out, we need to do it for all n,
since we won't know which n our inner expressions evaluate to.

So, let us prove this lemma:

e |-> e' implies (∀n. e' ↓ n implies e ↓ n)

This is again done by rule induction on the premise e |-> e', with
Q(e, e') ≡ (∀n. e' ↓ n implies e ↓ n)

This induction will involve 8 cases, one for each case of the small-step
relation. That's quite a few. Let's just look at one of them.

Here is the right-context rule for Plus:

         e2 |-> e2'
-----------------------------------
Plus (Num i) e2 |-> Plus (Num i) e2'

This rule lets us take a small step on the right hand side of a plus, assuming
the left hand side has already been evaluated down to a number.

The induction case associated with this rule has the IH
Q(e2, e2') ≡ (∀n. e2' ↓ n implies e2 ↓ n),
and the goal
Q(∀n. Plus (Num i) e2' ↓ n implies Plus (Num i) e2 ↓ n).

So, we fix n and assume our plus premise Plus (Num i) e2' ↓ n.

We now do a case-split on the derivation of this premise. Case splitting is
like induction, it requires a judgement and gives us goals for the different
cases by which this judgement might have been derived. The case-split
principle can be derived from the induction principle, roughly speaking, by
keeping the assumptions that the goal is of a particular shape and throwing the
IHs away.

Anyway, we have the plus premise Plus (Num i) e2' ↓ n. There is only one
big-step rule that can derive a big-step of a Plus.

This rule requires Num i ↓ v1 and e2' ↓ v2 (for some new v1 and v2) and shows
Plus (Num i) e2' ↓ (v1 + v2). So, we can assume that our n is v1 + v2 and that
we have Num i ↓ v1 and e2' ↓ v2. We could also case split on Num i ↓ v1, but
we don't need to for this proof.

We can now complete our derivation:

                          ------------ case-assum
                            e2' ↓ v2
  ----------- case-assum  ------------ IH
   Num i ↓ v1               e2 ↓ v2
  ------------------------------------ big-step Plus rule  ----------- case-assum
      Plus (Num i) e2 ↓ v1 + v2                            n = v1 + v2
      ---------------------------------------------------------------- subst of =
                 Plus (Num i) e2 ↓ n

That proves this case of our lemma proof.

The other "context cases" are all similar. Presume that e |-> e' and that e' wrapped
in some context evaluates to n, e.g. Let nm e' e2 ↓ n. Also assume the IH that
e' and e always big-step to the same n. Do cases on how e' in context evaluate big-step,
and specialise the problem to the relevant case, which will include a big-step
evaluation of e'. Apply the IH to get that e evaluates to the same thing alone, and
then use the rule for this case to give an evaluation of e in context. Finally,
observe that these evaluations in context gave the same value.

The "progress cases", e.g. the step Plus (Num i) (Num j) |-> Num (i + j) will
be provable by simply using the relevant big-step rule.

The slides claim the reverse case, assuming a big step evaluation, is easy. I
tried this in another framework, and I claim it is fiddly too. We will talk
about that briefly on Thursday.

2024-11-28 Thu 20:06

Announcements RSS